package mkv.data 
{
	import flash.display.Loader;
	import flash.events.*;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	
	/**
	 * FFON - Flat File Object Notation Serializer
	 * Parses in a text file defined in FFON format into hashed objects and arrays.
	 * Designed as a half-way house between JSON and INI files - easy to read and write, without complex formatting.
	 * 
	 * Primary goal: to make a human readable, editable, data format that can be parsed into a simple 2-3 level object hierarchy.
	 * 
	 * Example format:
	 * [Config]
	 * path: c:\Users\John\Instructions\
	 * debug: true
	 * version: 1.0.0
	 * 
	 * [Person]
	 * name: John Beech
	 * age: 25
	 * email: johnbeech@mkv25.net
	 * 
	 * [Person]
	 * name: Joe Bloggs
	 * age: 30
	 * email: joeblogs@mkv25.net
	 *
	 * Equivalent to JSON:
	 * data: {
	 *   Config: {path:"c:\\Users\\John\\Instructions\\", debug: true, version: "1.0.0"}
	 *   Person: [
	 *     {name:"John Beech", age:25, email:"johnbeech@mkv25.net"},
	 * 	   {name:"Joe Bloggs", age:30, email:"joebloggs@mkv25.net"}
	 *   ]
	 * }
	 * 
	 * The new version of the Serializer also supports nested object paths in a dot (.) separated form.
     * [Config]
	 * path: c:\Users\John\Instructions\
	 * debug: true
	 * version: 1.0.0
	 * complex.path: 5
	 * complex.date: 21/05/1985
	 * complex.author.firstName: John
	 * complex.author.lastName: Beech
	 * 
	 * @author John Beech
	 */
	public class FFONSerializer {
		
		/* Parser constants */
		protected static var VALID_LETTERS:String = 'abcdefghijklmnopqrstuvwxyz';
		
		/* Flag to turn parser comments on and off */
		public static const VERBOSE_TRACE:Boolean = false;
		
		/* Internal protected properties */
		protected var callbackSuccess:Function;
		protected var callbackError:Function;
		protected var microformats:Object;
		protected var currentLine:int;
		protected var currentPath:Array;
		protected var targetPath:Array;
		
		/* Public properties */
		/** the path currently being loadde by this instance of FFONSerializer */
		public var filepath:String;
		/** the loader that accesses the file stream */
		public var loader:URLLoader;
		/** the parsed data object */
		public var data:Object;
		/** Set this flag to look for automatically parse microformats from a file - to use add a [Microformats] section and define each microformat on a line */
		public var parseMicroformats:Boolean;
	
		public function FFONSerializer(parseMicroformats:Boolean=false) {
			microformats = { };
			this.parseMicroformats = parseMicroformats;
		}
		
		/**
		 * Load and deserialize a FFON formatted object
		 * @param	filepath
		 * @param	callbackSuccess
		 * @param	callbackError
		 */
		public function Load(filepath:String, callbackSuccess:Function=null, callbackError:Function=null):void {
			loader = new URLLoader();
			loader.dataFormat = URLLoaderDataFormat.TEXT;
			
			loader.addEventListener(Event.COMPLETE, LoadSuccess, false, 0, true);
			
			loader.addEventListener(IOErrorEvent.IO_ERROR, LoadError, false, 0, true);
			loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, LoadError, false, 0, true);
			loader.addEventListener(IOErrorEvent.NETWORK_ERROR, LoadError, false, 0, true);
			
			this.filepath = filepath;
			this.callbackSuccess = callbackSuccess;
			this.callbackError = callbackError;
			
			var url:URLRequest = new URLRequest(filepath);
			loader.load(url);
		}
		
		/**
		 * Handle successful file load event, attempt to deserialize, and call the callback function if set.
		 * Callback is called with a reference to this FFONSerializer object.
		 * @param	evt   the complete event sent from a valid URLLoader object
		 */
		public function LoadSuccess(evt:Event):void {
			var loader:URLLoader = URLLoader(evt.target);
			data = Deserialize(loader.data);
			
			if (callbackSuccess != null)
			{
				callbackSuccess(this);
			}
		}
		
		/**
		 * Handle IOErrorEvent and SecurityErrorEvent in the case of being unable to load a file.
		 * Callback is called with a reference to this FFONSerializer object.
		 * @param	evt
		 */
		public function LoadError(evt:Event):void {
			evt.stopPropagation();
			
			if (callbackError != null) {
				callbackError(this);
			}
		}
		
		public function AddMicroformat(microformat:Microformat):void {
			microformats[microformat.match] = microformat;
			
			if (FFONSerializer.VERBOSE_TRACE) {
				trace("Add microformat: (" + microformat.match + ")");
			}
		}
		
		/**
		 * Removes a microformat from this instance of FFONSerializer.
		 * @param	microformat  can be a string identifying the microformat, or an instance of a microformat object
		 */
		public function RemoveMicroformat(microformat:*):void {
			if (microformat is Microformat) {
				microformats[microformat.match] = null;
			}
			else {
				microformats[microformat] = null;
			}
		}
		
		/**
		 * Converts an object into serialized string data reverse compatable with the Deserialize function
		 * @param	data
		 * @return
		 */
		public function Serialize(data:Object):String {
			return SerializeWorker(data);
		}
		
		/**
		 * Converts an object into serialized string data.
		 * @param	data     the data object to modify
		 * @param	keyPath  the current key path
		 * @param   level    the recursion level 
		 * @return
		 */
		private function SerializeWorker(data:Object, keyPath:Array=null, level:int=0):String {
			var result:String = "";
			var NL:String = "\n";
			
			var value:*;
			var entry:*;
			var flatKeyPath:String;
			
			var sectionCount:int = 0;
			var propertyCount:int = 0;
			
			currentPath = keyPath;
			
			if (level == 0) {
				if(FFONSerializer.VERBOSE_TRACE) {
					trace("Serialize data: ");
					trace("============================");
					for each(var mf:Microformat in microformats)
					{
						trace("Microformat: " + mf.match + ": " + mf.micropattern);
					}
					trace("============================");
				}
				
				keyPath = [];
			}
			
			for (var key:String in data) {
				value = data[key];
				
				if(keyPath.length > 1) {
					flatKeyPath = "";
					for (var i:int = 1; i < keyPath.length; i++) {
						if (i > 1) {
							flatKeyPath = flatKeyPath + ".";
						}
						flatKeyPath = flatKeyPath + keyPath[i];
					}
					flatKeyPath = flatKeyPath + "." + key;
				}
				else {
					flatKeyPath = key;
				}
				
				if (value is Array) {
					if (level == 0) {
						for each(entry in value) {
							result += NL + "[" + key  + "]" + NL;
							result += SerializeWorker(entry, keyPath.concat(key), level + 1);
							
							sectionCount++;
						}
					}
					else {
						for each(entry in value) {
							/* Format microformat if matched */
							if (microformats[key] != null) {
								entry = microformats[key].Serialize(entry);
							}
							
							result += flatKeyPath + ": " + entry + NL;
							
							propertyCount++;
						}
					}
				}
				else if(value is Number || value is Boolean || value is String || value is int || value is uint) {
					/* Format microformat if matched */
					if (microformats[key] != null) {
						entry = microformats[key].Serialize(value);
					}
					
					result += flatKeyPath + ": " + value + NL;
					
					propertyCount++;
				}
				else {
					/* Handle object */
					if (level == 0) {
						result += NL + "[" + key + "]" + NL;
						
						sectionCount++;
					}
					
					if(microformats[key] != null) {
						/* Format microformat if matched */
						value = microformats[key].Serialize(value);
						
						result += flatKeyPath + ": " + value + NL;
					}
					else {
						/* Recurse down another level */
						result += SerializeWorker(value, keyPath.concat(key), level + 1);
					}
				}
			}
			
			if (keyPath.length == 0 && FFONSerializer.VERBOSE_TRACE) {
				trace(result);
				trace("============================");
				trace("FFONSerialiser.Serialize: Serialized " + sectionCount + " sections");
				trace("============================");
			}
			
			return result;
		}
		
		/**
		 * Convert a FFON formatted data file into serialized data
		 * @param	data
		 * @return  a structured object containing named keys and arrays from the source data
		 */
		public function Deserialize(data:String):Object {
			var result:Object;
			
			if (FFONSerializer.VERBOSE_TRACE) {
				trace("Deserialize input: ");
				trace("============================");
				trace(data);
				trace("============================");
			}
			
			var lines:Array = data.split("\n");
			var resultPath:Object;
			var subPath:Object;
			var section:String;
			var i:int, j:int;
			var key:String, value:*;
			var keyPath:Array;
			var sectionCount:int, propertyCount:int;
			
			result = { };
			resultPath = result;
			section = '';
			sectionCount = 0;
			propertyCount = 0;
			currentLine = 0;
			currentPath = [];
			targetPath = [];
			
			for each(var line:String in lines) {
				/* Check for new [section] */
				if (line.charAt(0) == '[') {
					/* Check for valid end of [section] */
					j = line.indexOf(']');
					if (j != -1) {
						/* Record section name */
						line = FFONSerializer.trim(line);
						section = line.substr(1, j - 1);
						
						resultPath = { };
						this.AddProperty(result, section, resultPath);
						
						if (FFONSerializer.VERBOSE_TRACE) {
							trace("Section: " + section);
						}
						sectionCount++;
						
						currentPath = [section];
					}
				}
				else if (FFONSerializer.VALID_LETTERS.indexOf(line.charAt(0).toLowerCase()) != -1) {
					i = line.indexOf(':');
					if (i != -1) {
						/* Decode property; key: value pair */
						key = line.substr(0, i);
						value = line.substr(i + 1);
						
						key = FFONSerializer.trim(key);
						value = FFONSerializer.trim(value);
						
						/* Check for complex key path */
						j = key.indexOf(".");
						if (j != -1) {
							keyPath = key.split(".");
							key = keyPath[keyPath.length - 1];
							
							currentPath = targetPath;
							targetPath = [].concat(keyPath);
						}
						else {
							keyPath = null;
						}
						
						/* Save / create microformat */
						if (parseMicroformats == true && section == 'Microformats') {
							var mf:Microformat = new Microformat(value, key);
							AddMicroformat(mf);
						}
						else {
							/* Apply microformat if available */
							if (microformats[key] != null) {
								value = microformats[key].Deserialize(value);
							}
						}
						
						/* Add property to data structure */
						if (keyPath != null) {
							subPath = this.GetPath(resultPath, keyPath);
							this.AddProperty(subPath, key, value);
						}
						else {
							this.AddProperty(resultPath, key, value);	
						}
						
						propertyCount++;
					}
					else {
						/* Not a valid property, no : found */
					}
				}
				else {
					/* Line started with invalid letter for a property name */
				}
				currentLine = currentLine + 1;
			}
			
			if (FFONSerializer.VERBOSE_TRACE) {
				trace("============================");
				trace("FFONSerialiser.Deserialize: Parsed " + sectionCount + " sections and " + propertyCount + " properties from " + lines.length + " lines");
				trace("============================");
			}
			
			return result;
		}
		
		/**
		 * Modify a data object to include a new property.
		 * If property already exists, change it into an array and list the values
		 * @param	path      the data object to be modified
		 * @param	key       the property name (key)
		 * @param	value     the propery value (value)
		 */
		protected function AddProperty(path:Object, key:String, value:*):void {
			var path:*;
			
			if (path.hasOwnProperty(key) == false) {
				if (path is Number || path is Boolean || path is String || path is int || path is uint) {
					throw new Error("Cannot add sub property: (" + targetPath.join(".") + ": " + value + ") to an existing value: (" + currentPath.join(".") + ": " + path + "). Error in FFON File: (" + filepath + ") at line: " + currentLine);
				}
				if (path is Array) {
					throw new Error("Cannot add sub property: (" + targetPath.join(".") + ": " + value + ") to an existing array: (" + currentPath.join(".") + ": " + path + "). Error in FFON File: (" + filepath + ") at line: " + currentLine);
				}
				/* Assign new property */
				path[key] = value;
			}
			else if (path[key] is Array) {
				/* Append new property */
				path[key].push(value);
			}
			else {
				/* Convert existing property to array and append new value */
				path[key] = [path[key]];
				path[key].push(value);
			}
		}
		
		/**
		 * Modify a data object to include path information
		 * @param	start    the starting path object to modify
		 * @param	keyPath  an array of path stages
		 * @return  the resulting end point of the modified path
		 */
		protected function GetPath(start:Object, keyPath:Array):Object {
			var path:Object = start;
			var key:String;
			
			while (keyPath.length > 1) {
				key = keyPath[0];
				keyPath.splice(0, 1);
				
				if (path[key] == null) {
					path[key] = { };
				}
				path = path[key];
			}
			return path;
		}
		
		protected static function trim(s:String):String {
			return s.replace(/^\s+|\s+$/gs, '');
		}
	}

}